<?php
/**
 * calendar base class
 * NOTE it only outputs the main calendar body,
 * controls must be built by using the apis
 * so different calendars may be built by extending this one.
 *
 * @package com.bbqq
 * @author  Bruce Li <toughgamer@gmail.com>
 *
 * @copyright 2009 qstufie.com
 */
class BBQQ_Calendar
{
    /**
     * constant: format of the day
     * @const
     */
    const DAYFORMAT = 'j';


    const RAWTODAY = '★';


    /**
     * is it raw output? if so, output text only calendar
     * @var bool
     */
    protected $_isRaw = false;


    /**
     * base date
     * @var array
     */
    protected $_baseDate = array();


    /**
     * today's date
     * @var array
     */
    protected $_today = array();


    /**
     * highlighted days
     * @var array
     */
    protected $_highlightedDays = array();


    /**
     * main wrapper
     * @var array
     */
    protected $_mainWrapper = array(
        'tag'         => 'table',
        'id'          => 'calendar',
        'class'       => 'calendar',
        'title'       => 'the calendar',
        'border'      => '0',
        'cellpadding' => '0',
        'cellspacing' => '0',
    );


    /**
     * default cellwrapper
     * @var array
     */
    protected $_cellWrapper = array(
        'tag'    => 'td',
        'id'     => null,
        'class'  => null,
        'title'  => ':title',
        'format' => 'jS M, Y' // this refers to the title format
    );


    /**
     * default empty cellwrapper
     * @var array
     */
    protected $_emptyCellWrapper = array(
        'tag'    => 'td',
        'id'     => null,
        'class'  => 'empty',
        'title'  => '',
        'format' => ''
    );


    /**
     * default cell highlighter
     * @var array
     */
    protected $_cellHighlighter = array(
        'tag'    => 'td',
        'id'     => null,
        'class'  => 'active',
        'title'  => ':title',
        'format' => 'jS M, Y' // this refers to the title format
    );


    /**
     * start day of week, default sunday
     * @var str
     */
    protected $_weekStart = 'sun';


    /**
     * week day titles
     * @var array
     */
    protected $_weekDayTitle = array(
        'sun' => 'Sun',
        'mon' => 'Mon',
        'tue' => 'Tue',
        'wed' => 'Wed',
        'thu' => 'Thu',
        'fri' => 'Fri',
        'sat' => 'Sat',
    );


    /**
     * order of the weekday array
     * @var array
     */
    protected $_weekDayOrder = array(
        'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'
    );


    /**
     * order days... flip of weekday array
     * @var array
     */
    protected $_orderDays = array();


    /**
     * week day titles
     * @var array
     */
    protected $_weekDayTitleWrapper = array(
        'sun' => array(
            'tag' => 'th',
            'id'    => null,
            'class' => 'cal-sun',
            'title' => 'Sunday'
        ),
        'mon' => array(
            'tag' => 'th',
            'id'    => null,
            'class' => 'cal-mon',
            'title' => 'Monday'
        ),
        'tue' => array(
            'tag' => 'th',
            'id'    => null,
            'class' => 'cal-tue',
            'title' => 'Tuesday'
        ),
        'wed' => array(
            'tag' => 'th',
            'id'    => null,
            'class' => 'cal-wed',
            'title' => 'Wednesday'
        ),
        'thu' => array(
            'tag' => 'th',
            'id'    => null,
            'class' => 'cal-thu',
            'title' => 'Thursday'
        ),
        'fri' => array(
            'tag' => 'th',
            'id'    => null,
            'class' => 'cal-fri',
            'title' => 'Friday'
        ),
        'sat' => array(
            'tag' => 'th',
            'id'    => null,
            'class' => 'cal-sat',
            'title' => 'Saturday'
        ),
    );

/*----------------------------------[cosntructor]----------------------------------*/

    /**
     * constructor
     * all we need to start a calendar
     *
     * @param int(linux timestamp) $date         date the calendar is based on
     * @param int(linux timestamp) $today        today's timestamp
     * @param str                  $weekStart    start of the week
     * @param array                $weekDayTitle titles to display
     *
     * @return void
     */
    public function __construct($date = null, $today = null, $weekStart = 'sun', $weekDayTitle = array())
    {
        // config
        $this->setBaseDate($date)
             ->setToday($today)
             ->setWeekStart($weekStart)
             ->setWeekDayTitle($weekDayTitle);

        // reorder weekday headers...
        $this->_orderDays =
        $OrderDays        = array_flip($this->_weekDayOrder);
        $index            = $OrderDays[$weekStart];

        // get this one out and resort it...
        $rightChunk = array_slice($this->_weekDayOrder, $index);
        $leftChunk  = array_slice($this->_weekDayOrder, 0, $index);

        // combine
        $this->_weekDayOrder = array_merge($rightChunk, $leftChunk);

        // week day titles
        $this->setWeekDayTitle($weekDayTitle);

    }// constructor

/*----------------------------------[config]----------------------------------*/

    /**
     * set weekday titles
     *
     * @param array $weekDayTitle
     *
     * @return BBQQ_Calendar
     */
    public function setWeekDayTitle($weekDayTitle)
    {
        if (!is_array($weekDayTitle)) {
            throw new Exception('WEEK DAY TITLE IS NOT VALID');
        }

        // otherwise...
        foreach ($weekDayTitle as $k => $v) {
            if (isset($this->_weekDayTitle[$k])) {
                $this->_weekDayTitle[$k] = $v;
            }
        }

        return $this;

    }// end setWeekDayTitle


    /**
     * set today's date
     * must use linux timestamp
     *
     * @param int $today
     *
     * @return BBQQ_Calendar
     */
    public function setToday($today)
    {
        $this->_today = $this->_breakDate((int) $today);
        return $this;

    }// end setToday


    /**
     * set base date
     * must use linux timestamp
     *
     * @param int $baseDate
     *
     * @return BBQQ_Calendar
     */
    public function setBaseDate($baseDate)
    {
        $this->_baseDate = $this->_breakDate((int) $baseDate);
        return $this;

    }// end setBaseDate


    /**
     * set weekstart string
     *
     * @param str $weekStart
     *
     * @return BBQQ_Calendar
     */
    public function setWeekStart($weekStart)
    {
        $weekStart = (string) $weekStart;
        if (isset($this->_weekDayTitle[$weekStart])) {
            $this->_weekStart = $weekStart;
        }

        return $this;

    }// end setWeekStart


    /**
     * set cell wrapper options
     *
     * @param array $wrapper
     * @return $this
     */
    public function setCellWrapper($wrapper)
    {
        $this->_cellWrapper = $wrapper;
        return $this;

    }// end setCellWrapper


    /**
     * set cell highlighter wrapper options
     *
     * @param array $wrapper
     * @return BBQQ_Calendar
     */
    public function setCellHighlighter($wrapper)
    {
        $this->_cellHighlighter = $wrapper;
        return $this;

    }// end setCellHighlighter


    /**
     * set weekday titles wrapper options
     *
     * @param array $wrapper
     * @return BBQQ_Calendar
     */
    public function setWeekDayTitleWrappers($wrapper)
    {
        $this->_weekDayTitleWrapper = $wrapper;
        return $this;

    }// end setCellHighlighter


    /**
     * set main wrapper options
     *
     * @param array $wrapper
     * @return BBQQ_Calendar
     */
    public function setMainWrapper($wrapper)
    {
        $this->_mainWrapper = $wrapper;
        return $this;

    }// end setCellHighlighter

/*----------------------------------[public apis]----------------------------------*/

    /**
     * get today formatted
     * NOTE the 'today' term here refers to the
     * give '$today' var in the constructor, may
     * not be exactly today
     *
     * @param str  $format
     * @param bool $raw if true, return raw timestamp
     *
     * @return str
     */
    public function getToday($format = 'jS M, Y', $raw = false)
    {
        return $this->_exportDate($format, $this->_today['RAW'], $raw);

    }// end getToday


    /**
     * get tomorrow formatted
     *
     * @param str  $format
     * @param bool $raw if true, return raw timestamp
     *
     * @return str
     */
    public function getTomorrow($format = 'jS M, Y', $raw = false)
    {
        return $this->_exportDate($format, ($this->_today['RAW'] + 3600 * 24), $raw);

    }// end getTomorrow


    /**
     * get Yesterday formatted
     *
     * @param str  $format
     * @param bool $raw if true, return raw timestamp
     *
     * @return str
     */
    public function getYesterday($format = 'jS M, Y', $raw = false)
    {
        return $this->_exportDate($format, ($this->_today['RAW'] - 3600 * 24), $raw);

    }// end getYesterday


    /**
     * get next month
     *
     * @param str  $format
     * @param bool $raw if true, return raw timestamp
     *
     * @return str
     */
    public function getNextMonth($format = 'F Y', $raw = false)
    {
        return $this->_exportDate($format, strtotime('next month', $this->_today['RAW']), $raw);

    }// end str


    /**
     * get previous month
     *
     * @param str  $format
     * @param bool $raw if true, return raw timestamp
     *
     * @return str
     */
    public function getPreviousMonth($format = 'F Y', $raw = false)
    {
        return $this->_exportDate($format, strtotime('last month', $this->_today['RAW']), $raw);

    }// end getPreviousMonth


    /**
     * get start day of the month
     *
     * @param str  $format
     * @param bool $raw if true, return raw timestamp
     *
     * @return str
     */
    public function getStartDay($format = 'jS F Y', $raw = false)
    {
        $day = mktime(1, 0, 0, $this->_baseDate['m'], 1, $this->_baseDate['Y']);
        return $this->_exportDate($format, $day, $raw);

    }// end getStartDay


    /**
     * get end day of the month
     *
     * @param str  $format
     * @param bool $raw if true, return raw timestamp
     *
     * @return str
     */
    public function getEndDay($format = 'jS F Y', $raw = false)
    {
        $day = mktime(1, 0, 0, $this->_baseDate['m'] + 1, 0, $this->_baseDate['Y']);
        return $this->_exportDate($format, $day, $raw);


    }// end getEndDay


    /**
     * render the calendar
     * based on given params
     *
     * @param bool  $export     if true, export it, else, echo it
     * @param bool  $raw        if true, return a geektool/pre friendly raw text calendar
     * @param str   $custHeader to replace the default header
     *
     * @return mixed
     */
    public function render($export = true, $raw = false, $custHeader = null)
    {
        // cal..
        $cal = '';

        // tags
        $nlStart = "\n<tr>";
        $nlEnd   = "</tr>\n";

        // if raw...
        if ($raw) {
            $nlStart = "\n";
            $nlEnd   = null;
        }

        // raw?
        $this->_isRaw = $raw;

        // build header...
        if (empty($custHeader)) {
            $cal .= $this->buildHeader();
        } else {
            $cal .= $custHeader;
        }

        // do the actual thingy here...
        // 1. how many days before it gets to first of month?
        $baseDate = $this->_baseDate;
        $startDay = $this->getStartDay(null, 1);
        $endDay   = $this->getEndDay(null, 1);
        // int value...
        $startDayInt = 1;
        $endDayInt   = date('d', $endDay);

        // 2. how many empty cells???
        $startDayKey = $this->_orderDays[strtolower(date('D', $startDay))];
        $endDayKey   = $this->_orderDays[strtolower(date('D', $endDay))];

        // 3. start of the rows...
        $cnt  = 1;
        if ($startDayKey > 0) {
            $cal .= $nlStart;
            for ($i = 0; $i < $startDayKey; $i ++) {
                $cal .= $this->buildCell(false);
                $cnt ++;
            }
        }

        // 4. big loop
        $curdate = $startDay;
        for ($num = 1; $num <= $endDayInt; $num ++) {
            if ($cnt == 1) {
                $cal .= $nlStart;
            }

            $cal .= $this->buildCell($curdate);
            $curdate += 3600 * 24;

            if ($cnt == 7) {
                $cal .= $nlEnd;
                $cnt = 0;
            }

            $cnt ++;
        }

        // 5. fix the end of it...
        if ($endDayKey < 6) {
            for ($i = $endDayKey; $i < 6; $i ++) {
                $cal .= $this->buildCell(false);
            }
            $cal .= $nlEnd;
        }

        // export or echo...
        if (!$raw) {
            $cal = $this->buildTag($this->_mainWrapper, $cal);
        }

        if ($export) {
            return $cal;
        }

        echo $cal;

    }// end render

/*----------------------------------[special apis]----------------------------------*/

    /**
     * hightlight days
     *
     * @param array $days must be in the following format
     * array([day] => array(
     *                                  'template' => [template str],
     *                                  'wrapper' => [customised tag wrapper array],
     *                                  )
     * FOR TEMPLATE:
     * > use :num       for the actual day number
     * > use :timestamp for the actual time stamp
     *
     * @example highlightDays(
     *                        array(1 => array(
     *                              'template' => '<a href="event?id=event_:timestamp">:num</a>',
     *                              'wrapper' => array(
     *                                  'tag' => 'th',
     *                                  'id'    => null,
     *                                  'class' => 'cal-thu',
     *                                  'format' => 'j F, Y',
     *                                  'title' => 'some :title'
     *                              )
     *                          )
     *                        );
     *
     * @return void
     */
    public function highlightDays($days)
    {
        $this->_highlightedDays = $days;

    }// end highlightDays

/*----------------------------------[private methods]----------------------------------*/

    /**
     * break timestamp into array
     *
     * @param str(linux_timestamp) $date default null
     *
     * @return array
     */
    private function _breakDate($date = null)
    {
        if (empty($date)) {
            $date = time();
        }

        return array(
            'RAW' => $date, // keep raw here to speed up the process
            'Y' => date('Y', $date),
            'm' => date('m', $date),
            'd' => date(self::DAYFORMAT, $date)
        );

    }// end _breakDate


    /**
     * export date to destined format
     *
     * @param str  $format
     * @param int  $date
     * @param bool $raw
     *
     * @return str
     */
    private function _exportDate($format, $date, $raw = false)
    {
        if ($raw) {
            return (int) $date;
        }

        return date($format, (int) $date);

    }// end _exportDate

/*----------------------------------[protected methods - can be extended]----------------------------------*/

    /**
     * build tag with give wrapper and value...
     *
     * @param array $wrapper
     * @param str   $value
     *
     * @return str
     */
    protected function buildTag($wrapper, $value = null)
    {
        $tag = $wrapper['tag'];
        // unset tag
        unset($wrapper['tag']);

        $src = '<' . $tag;
        foreach ($wrapper as $k => $v) {
            $src .= ' ' . $k . '="' . $v . '"';
        }
        $src .= '>' . $value . '</' . $tag . '>' . "\n";
        return $src;


    }// end buildTag

    /**
     * build a cell based on the template...
     * do not use it to build headers...
     *
     * @param int(linux timestamp) $date the timestamp
     *
     * @return str
     */
    protected function buildCell($date)
    {
        // calculate
        $num       = null;
        $timestamp = $date;
        if ($date > 0) {
            $num = date(self::DAYFORMAT, $date);
        }

        // if raw...
        if ($this->_isRaw) {
            if ($this->_today['d'] == $num) {
                $num = self::RAWTODAY;
            }
            return $this->rawCell($num);
        }

        // default wrapper
        $wrapper = $this->_cellWrapper;

        // if empty cell...
        if (empty($date)) {
            // use default empty date wrapper
            return $this->buildTag($this->_emptyCellWrapper);
        }

        // the key parts:
        $title     = date($wrapper['format'], $date);

        // else, if it's today...
        if ($this->_today['d'] == $num) {
            // use cell highlighter wrapper
            $wrapper = $this->_cellHighlighter;
        }

        $value = $num;

        // add str to it... for highlighted stuff
        if (isset($this->_highlightedDays[$num]['template'])) {
            // then that's the new template to use
            $template = $this->_highlightedDays[$num]['template'];
            // wrapper
            if (isset($this->_highlightedDays[$num]['wrapper'])) {
                $wrapper  = $this->_highlightedDays[$num]['wrapper'];
            }
            // value...
            $value    = str_replace(array(':num', ':title', ':timestamp'),
                            array($num, $title, $timestamp),
                            $template
                        );
        }

        // build the tag...
        if (strpos($wrapper['title'], ':title') !== false) {
            $wrapper['title'] = str_replace(':title', $title, $wrapper['title']);
        }

        // return it out...
        // 1st, unset the format key
        unset($wrapper['format']);
        return $this->buildTag($wrapper, $value);

    }// end buildCell


    /**
     * build a raw cell out by using tabs...
     *
     * @param int(linux timestamp) $date the timestamp
     *
     * @return str
     */
    protected function rawCell($date)
    {
        // total...
        if (strlen($date) > 1) {
            $date = "$date\t";
        } else {
            $date = " $date\t";
        }

        return $date;

    }// end rawCell


    /**
     * build header
     *
     * @return str
     */
    protected function buildHeader()
    {
        $wrapper = $this->_weekDayTitleWrapper;

        // now build it...
        if ($this->_isRaw) {
            return $this->rawHeader();
        }

        // otherwise...
        $header = '<tr>';
        foreach ($this->_weekDayOrder as $wd) {
            $wrapper = $this->_weekDayTitleWrapper[$wd];
            $header .= $this->buildTag($wrapper, $this->_weekDayTitle[$wd]) . "\n";
        }

        return $header . '</tr>';

    }// end buildHeader


    /**
     * build a raw header out by using tabs...
     * @return str
     */
    protected function rawHeader()
    {
        // loop a loop...
        $header = null;
        foreach ($this->_weekDayOrder as $wd) {
            $header .= "{$this->_weekDayTitle[$wd]}   ";
        }
        return $header;

    }// end rawCell

}// end BBQQ_Calendar
?>